import threading
import inspect
import time
import asyncio
import socket
from uuid import uuid4

from redis import from_url as sync_from_url
from redis.asyncio import from_url as async_from_url

from config import settings
from utils import logger


class RedisClients:
    async_cli = async_from_url(
        socket_keepalive=True,
        socket_keepalive_options={
            socket.TCP_KEEPIDLE: 120,
            socket.TCP_KEEPCNT: 2,
            socket.TCP_KEEPINTVL: 30
        },
        **settings.redis
    )
    sync_cli = sync_from_url(
        socket_keepalive=True,
        socket_keepalive_options={
            socket.TCP_KEEPIDLE: 120,
            socket.TCP_KEEPCNT: 2,
            socket.TCP_KEEPINTVL: 30
        },
        **settings.redis
    )

    @staticmethod
    def _watch_dog(key, uid, expired):
        logger.debug("开启watch_dog线程")
        while hasattr(RedisClients, uid):
            ttl = RedisClients.sync_cli.ttl(key)
            get_uid = RedisClients.sync_cli.get(key)
            if ttl and get_uid == uid and ttl < 2:
                logger.debug("延长锁的时间")
                RedisClients.sync_cli.expire(key, expired)
            time.sleep(1)
        logger.debug("退出线程")

    @staticmethod
    def _start_watch_dog(key, uid, expired):
        th = threading.Thread(target=RedisClients._watch_dog, name=uid, args=(key, uid, expired), daemon=True)
        th.daemon = True
        th.start()
        return th

    @staticmethod
    def _sync_get_lock(key, expired):
        uid = uuid4().hex.upper()
        setattr(RedisClients, uid, True)
        while not RedisClients.sync_cli.set(key, uid, nx=True, ex=expired):
            time.sleep(1)
        logger.debug(f"获取{key}锁")
        return uid

    @staticmethod
    def _sync_release_lock(key, uid):
        get_uid = RedisClients.sync_cli.get(key)
        if get_uid == uid:
            RedisClients.sync_cli.delete(key)
            logger.debug(f"释放{key}锁")
            delattr(RedisClients, uid)

    @staticmethod
    async def _async_get_lock(key, expired):
        uid = uuid4().hex.upper()
        setattr(RedisClients, uid, True)
        while not await RedisClients.async_cli.set(key, uid, nx=True, ex=expired):
            await asyncio.sleep(1)
        logger.debug(f"获取{key}锁")
        return uid

    @staticmethod
    async def _async_release_lock(key, uid):
        get_uid = await RedisClients.async_cli.get(key)
        if get_uid == uid:
            await RedisClients.async_cli.delete(key)
            logger.debug(f"释放{key}锁")
            delattr(RedisClients, uid)

    @staticmethod
    def lock(key, expired=60):
        """
        分布式锁修饰器
        :param key: 锁的关键字
        :param expired: 锁的时间
        :return:
        """

        def wrap(func):
            async def async_gen_function(*args, **kwargs):
                uid = await RedisClients._async_get_lock(key, expired)

                try:
                    RedisClients._start_watch_dog(key, uid, expired)
                    async for _ in func(*args, **kwargs):
                        yield _
                except Exception as e:
                    raise e
                finally:
                    await RedisClients._async_release_lock(key, uid)

            async def async_exec_function(*args, **kwargs):
                uid = await RedisClients._async_get_lock(key, expired)

                try:
                    RedisClients._start_watch_dog(key, uid, expired)
                    exec_data = await func(*args, **kwargs)
                except Exception as e:
                    raise e
                else:
                    return exec_data
                finally:
                    await RedisClients._async_release_lock(key, uid)

            def exec_function(*args, **kwargs):
                uid = RedisClients._sync_get_lock(key, expired)
                try:
                    RedisClients._start_watch_dog(key, uid, expired)
                    exec_data = func(*args, **kwargs)
                except Exception as e:
                    raise e
                else:
                    return exec_data
                finally:
                    RedisClients._sync_release_lock(key, uid)

            if inspect.isasyncgenfunction(func):
                return async_gen_function
            elif inspect.iscoroutinefunction(func):
                return async_exec_function
            else:
                return exec_function

        return wrap
